<?php
/**
 * Svick a Fast Simple Smart PHP FrameWork
 * Author: Tommy 863758705@qq.com
 * Link: http://svick.tomener.com/
 * Since: 2022
 */

namespace Svick\Core;

use Closure;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use Svick\Exception\CoreException;

class Container
{
    /**
     * 容器绑定的标识
     *
     * @var array
     */
    protected $bindings = [];

    /**
     * 容器共享的实例
     *
     * @var array
     */
    protected $instances = [];

    /**
     * 注册的类型别名
     *
     * @var array
     */
    protected $aliases = [];

    /**
     * 向容器注册绑定
     *
     * @param string $abstract 类标识、接口
     * @param null $concrete 要绑定的类、闭包
     * @param bool $shared
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        $abstract = $this->normalize($abstract);
        $concrete = $this->normalize($concrete);

        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);
            $this->alias($abstract, $alias);
        }
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        $this->dropStaleInstances($abstract);
        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    /**
     * 注册一个共享服务到容器
     *
     * @param $abstract
     * @param null $concrete
     */
    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

    /**
     * 将一个已存在的类实例作为共享服务注册到容器
     *
     * @param string|array $abstract
     * @param $instance
     */
    public function instance($abstract, $instance)
    {
        $abstract = $this->normalize($abstract);

        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        unset($this->aliases[$abstract]);
        $this->instances[$abstract] = $instance;
    }

    /**
     * 为服务设置别名
     *
     * @param string $abstract
     * @param string $alias
     */
    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $this->normalize($abstract);
    }

    /**
     * 获取给定类型服务
     *
     * @param $abstract
     * @param array $params
     * @return mixed|object
     */
    public function get($abstract, array $params = [])
    {
        $abstract = $this->getAlias($this->normalize($abstract));

        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $concrete = $this->getConcrete($abstract);

        $object = $this->build($concrete, $params);

        if ($this->isShared($abstract)) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }

    /**
     * 调用反射执行类的方法
     *
     * @param array|string $method 方法
     * @param array $params 参数
     * @return mixed
     */
    public function invokeMethod($method, array $params = [])
    {
        if (is_array($method)) {
            [$class, $method] = $method;
            $class = is_object($class) ? $class : $this->get($class);
        } elseif (is_callable($method)) {
            $reflect = new ReflectionFunction($method);
            $args = $this->bindParams($reflect, $params);
            return $reflect->invokeArgs($args);
        } else {
            [$class, $method] = explode('::', $method);
        }

        try {
            $reflect = new ReflectionMethod($class, $method);
        } catch (ReflectionException $e) {
            $class = is_object($class) ? get_class($class) : $class;
            throw new CoreException('method not exists: ' . $class . '::' . $method . '()');
        }

        $args = $this->bindParams($reflect, $params);

        return $reflect->invokeArgs(is_object($class) ? $class : null, $args);
    }

    /**
     * 绑定参数
     *
     * @param ReflectionFunctionAbstract $reflect 反射类
     * @param array $params 参数
     * @return array
     */
    protected function bindParams(ReflectionFunctionAbstract $reflect, array $params = []): array
    {
        if ($reflect->getNumberOfParameters() == 0) {
            return [];
        }

        reset($params);
        $type = key($params) === 0 ? 0 : 1;
        $parameters = $reflect->getParameters();
        $args = [];

        foreach ($parameters as $parameter) {
            $name = $parameter->getName();
            $reflectionType = $parameter->getType();

            //isBuiltin判断是否是php内置类型
            if ($reflectionType && $reflectionType->isBuiltin() === false) {
                $args[] = $this->getObjectParam($reflectionType->getName(), $params);
            } elseif (0 == $type && !empty($params)) {
                $args[] = array_shift($params);
            } elseif (1 == $type && array_key_exists($name, $params)) {
                $args[] = $params[$name];
            } elseif ($parameter->isDefaultValueAvailable()) {
                $args[] = $parameter->getDefaultValue();
            } else {
                throw new CoreException(sprintf('Method %s is missing parameter %s', $reflect->getName(), $name));
            }
        }

        return $args;
    }

    /**
     * 获取对象类型的参数值
     *
     * @param string $className 类名
     * @param array $params 参数
     * @return mixed
     */
    protected function getObjectParam(string $className, array &$params)
    {
        $array = $params;
        $value = array_shift($array);

        if ($value instanceof $className) {
            $result = $value;
            array_shift($params);
        } else {
            $result = $this->get($className);
        }

        return $result;
    }

    /**
     * 获取给定类型服务实例
     *
     * @param string $concrete
     * @param array $params
     * @return object
     * @throws CoreException
     */
    public function build($concrete, array $params = [])
    {
        if ($concrete instanceof Closure) {
            return $concrete($this, $params);
        }

        $reflector = new ReflectionClass($concrete);

        if (!$reflector->isInstantiable()) {
            throw new CoreException("Class {$concrete} is not instantiable");
        }

        $constructor = $reflector->getConstructor();

        if (is_null($constructor)) {
            return $reflector->newInstance();
        }

        $parameters = $constructor->getParameters();
        $dependencies = $this->getDependencies($parameters, $params);

        return $reflector->newInstanceArgs($dependencies);
    }

    /**
     * 解析依赖关系
     *
     * @param array $parameters
     * @param array $params
     * @return array
     * @throws CoreException
     */
    protected function getDependencies(array $parameters, $params)
    {
        $dependencies = [];

        /**
         * @var $parameter \ReflectionParameter
         */
        foreach ($parameters as $key => $parameter) {
            $dependency = $parameter->getClass(); //return ReflectionClass Object
            if ($dependency === null) {
                if (!empty($params)) {
                    $dependencies[] = array_shift($params);
                } elseif ($parameter->isDefaultValueAvailable()) {
                    $dependencies[] = $parameter->getDefaultValue();//ReflectionParameter::getDefaultValue — Gets default parameter value
                } else {
                    throw new CoreException("Can not be resolve class dependency {$parameter->name}");
                }
            } else {
                $dependencies[] = $this->get($dependency->name);//递归调用
            }
        }

        return $dependencies;
    }

    /**
     * Get the concrete type for a given abstract.
     *
     * @param string $abstract
     * @return mixed   $concrete
     */
    protected function getConcrete($abstract)
    {
        if (!isset($this->bindings[$abstract])) {
            return $abstract;
        }

        return $this->bindings[$abstract]['concrete'];
    }

    /**
     * 通过别名获取服务名称
     *
     * @param string $abstract
     * @return string
     */
    protected function getAlias($abstract)
    {
        return $this->aliases[$abstract] ?? $abstract;
    }

    /**
     * Drop all of the stale instances and aliases.
     *
     * @param string $abstract
     * @return void
     */
    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

    /**
     * Determine if a given type is shared.
     *
     * @param string $abstract
     * @return bool
     */
    public function isShared($abstract)
    {
        $abstract = $this->normalize($abstract);

        if (!isset($this->bindings[$abstract]['shared'])) {
            return false;
        }

        return $this->bindings[$abstract]['shared'] === true;
    }

    /**
     * 判断是否设置了该别名
     *
     * @param string $name
     * @return bool
     */
    public function isAlias($name)
    {
        return isset($this->aliases[$name]);
    }

    /**
     * 确定给定的抽象类型是否已绑定
     *
     * @param string $abstract
     * @return bool
     */
    public function bound($abstract)
    {
        return isset($this->bindings[$abstract]) ||
            isset($this->instances[$abstract]) ||
            $this->isAlias($abstract);
    }

    /**
     * 是否绑定了该服务
     *
     * @param $abstract
     * @return bool
     */
    public function has($abstract)
    {
        return $this->bound($abstract);
    }

    /**
     * Extract the type and alias from a given definition.
     *
     * @param array $definition
     * @return array
     */
    protected function extractAlias(array $definition)
    {
        return [key($definition), current($definition)];
    }

    /**
     * Normalize the given class name by removing leading slashes.
     *
     * @param mixed $service
     * @return mixed
     */
    protected function normalize($service)
    {
        return is_string($service) ? ltrim($service, '\\') : $service;
    }
}
